How Qutebrowser Command Completion works

像 Vim 一样,当用户输入 : 后,qutebrowser 也会弹出一个命令提示视图(CompletionView),供用户交互式输入命令。如下图所示:

Screenshot from 2023-12-26 12-11-46.png

这一切背后的工作机制是怎样的呢?本文将分析这背后的工作原理。

Command KeyMode

如果你不了解 Qutebrowser KeyMode 的工作原理,建议先阅读:How Qutebrowser Modes works

Command KeyMode 的声明如下(modeman.py):

usertypes.KeyMode.command:
    modeparsers.CommandKeyParser(
        mode=usertypes.KeyMode.command,
        win_id=win_id,
        commandrunner=commandrunner,
        parent=modeman,
        passthrough=True,
        do_log=log_sensitive_keys,
        supports_count=False),

BaseKeyParser 的构造函数加载 command 模式下的按键映射。我从中挑选了一部分:

command:
  <Ctrl-P>: command-history-prev
  <Ctrl-N>: command-history-next
  <Up>: completion-item-focus --history prev
  <Down>: completion-item-focus --history next
  <Return>: command-accept
  <Ctrl-Return>: command-accept --rapid
  <Ctrl-B>: rl-backward-char
  <Ctrl-F>: rl-forward-char
  <Ctrl-Y>: rl-yank
  <Escape>: mode-leave
  # ...

响应冒号输入

要触发 command 模式,首先需要输入 :,需要注意,这时我们还处在 normal 模式。从 normal 模式的按键映射表中可以看到:

normal:
  <Escape>: clear-keychain ;; search ;; fullscreen --leave
  o: cmd-set-text -s :open
  # ...
  /: cmd-set-text /
  ?: cmd-set-text ?
  ":": "cmd-set-text :"

输入 : 将触发 cmd-set-text 命令。

cmd-set-text 命令

位于 mainwindow/statusbar/command.py

@cmdutils.register(instance='status-command', name='cmd-set-text',
                    scope='window', maxsplit=0, deprecated_name='set-cmd-text')
@cmdutils.argument('count', value=cmdutils.Value.count)
def cmd_set_text_command(self, text: str,
                            count: int = None,
                            space: bool = False,
                            append: bool = False,
                            run_on_count: bool = False) -> None:
    """Preset the statusbar to some text.
	...
    """
	#...

	# note: STARTCHARS = ":/?"
    if not text or text[0] not in modeparsers.STARTCHARS:
        raise cmdutils.CommandError(
            "Invalid command text '{}'.".format(text))
    if run_on_count and count is not None:
        # ...
    else:
        self.cmd_set_text(text)

def cmd_set_text(self, text: str) -> None:
    """Preset the statusbar to some text.
    """
    # update the text from StatusBar's Command view 
    self.setText(text)
    log.modes.debug("Setting command text, focusing {!r}".format(self))
    # go into command mode
    modeman.enter(self._win_id, usertypes.KeyMode.command, 'cmd focus')
    self.setFocus()
    # show command signal
    self.show_cmd.emit()

在上面代码中:

show_cmd signal

在 StatusBar 的构造函数,注册了 show_cmd 信号的监听:

self.cmd.show_cmd.connect(self._show_cmd_widget)
self.cmd.hide_cmd.connect(self._hide_cmd_widget)

_show_cmd_widget

def _show_cmd_widget(self):
    """Show command widget instead of temporary text."""
    self._stack.setCurrentWidget(self.cmd)
    self.show()

self.cmd 的类型是 Command,他是 CommandLineEdit 的子类,是一个单行输入框。

Command 输入框

现在 Command 输入框已经在 Statusbar 中展示出来,并带有初始文本 :。接下来,用户在其中输入命令,这会触发:

self.cursorPositionChanged.connect(self.update_completion)
self.textChanged.connect(self.update_completion)
self.textChanged.connect(self.updateGeometry)
self.textChanged.connect(self._incremental_search)

update_completion 是一个信号,接收方位于 Completer 类中,从类名中可以看出,Completer 专门负责命令补全。关于 update_completion 的响应将在下节中介绍。

我们先看 _incremental_search

@pyqtSlot()
def _incremental_search(self) -> None:
    if not config.val.search.incremental:
        return

    self._handle_search()

def _handle_search(self) -> bool:
    """Check if the currently entered text is a search, and if so, run it.

    Return:
        True if a search was executed, False otherwise.
    """
    if self.prefix() == '/':
        self.got_search.emit(self.text()[1:], False)
        return True
    elif self.prefix() == '?':
        self.got_search.emit(self.text()[1:], True)
        return True
    else:
        return False

从中可以看出,如果是搜索命令(以 /?)为开头,则触发 got_search 信号。

Completer.update_completion

在 Completer 的构造函数中,注册了 Command 的 update_completion 信号:

self._cmd.update_completion.connect(self.schedule_completion_update)

schedule_completion_update

@pyqtSlot()
def schedule_completion_update(self):
    """Schedule updating/enabling completion.
    """
    _cmd, _sep, rest = self._cmd.text().partition(' ')
    input_length = len(rest)
    if (0 < input_length < config.val.completion.min_chars and
            self._cmd.cursorPosition() > self._last_cursor_pos):
        log.completion.debug("Ignoring update because the length of "
                                "the text is less than completion.min_chars.")
    elif (self._cmd.cursorPosition() == self._last_cursor_pos and
            self._cmd.text() == self._last_text):
        log.completion.debug("Ignoring update because there were no "
                                "changes.")
    else:
        log.completion.debug("Scheduling completion update.")
        start_delay = config.val.completion.delay if self._last_text else 0
        self._timer.start(start_delay)
    self._last_cursor_pos = self._cmd.cursorPosition()
    self._last_text = self._cmd.text()

如果内容发生改变,并触发开始补全的阈值,则通过定时器 _timer 异步触发补全。

_timer 关联的槽函数是 _update_completion

@pyqtSlot()
def _update_completion(self):
    """Check if completions are available and activate them."""
    # get CompletionView
    completion = self._completion()

    # ...

    before_cursor, pattern, after_cursor = self._partition()

    # ...

    pattern = pattern.strip("'\"")
    # Get the completion function based on the current command text.
    func = self._get_new_completion(before_cursor, pattern)

    # ...

    self._last_before_cursor = before_cursor

    args = (x for x in before_cursor[1:] if not x.startswith('-'))
    cur_tab = objreg.get('tab', scope='tab', window=self._win_id,
                            tab='current')

    with debug.log_time(log.completion, 'Starting {} completion'
                        .format(func.__name__)):
        info = CompletionInfo(config=config.instance,
                                keyconf=config.key_instance,
                                win_id=self._win_id,
                                cur_tab=cur_tab)
        model = func(*args, info=info)
    with debug.log_time(log.completion, 'Set completion model'):
        completion.set_model(model)

    completion.set_pattern(pattern)

本文作者:Maeiee

本文链接:How Qutebrowser Command Completion works

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!